타입 정보 제공 및 사용모든 템플릿 함수와 클래스에는 가능한 모든 인수 타입에 대해 동일한 코드를 포함한다.
템플릿의 표현력을 높이기 위해서 인수 타입에 따라 더 작거나 더 큰 코드 변형을 도입한다.
이를 위해서 타입의 정보가 필요하다.
타입 정보는 is_const나 is_reference처럼 기술적인 정보 또는
is_matrix나 is_pressure와 같은 의미/도메인별 정보가 있다.
라이브러리에서 지원하는 대부분의 기술적인 타입 정보는 <type_traits> 및 <limits> 헤더에 저장되어 있다.
타입 특성1개의 값 중 최소 크기를 갖는 값을 반환하는 함수
template <typename T>
T inline min_magnitude(const T& x, const T& y){
using std::abs;
T ax=abs(x), ay=abs(y);
return ax<ay? x: y;
}
위 함수는 int, unsigned, double 등의 타입으로 호출가능하다.
(컴파일러는 각 타입별로 코드를 생성함)
double d1=3., d2=4.;
cout<<"min | d1, d2| = "<<min_magniute(d1, d2)<<'\n';
하지만 complex값으로 호출할 경우 오류를 출력하게 된다.
std::complex<double> c1(3.), c2(4.);
cout<<"min | c1, c2 | = "<<min_magniute(c1, c2)<<'\n';
min_magniutde에서 abs는 비교 연산자를 제공하는 double 값으로 반환하지만,
결과를 complex값에 저장하였고, 이를 비교 연산자를 통해서 비교하였다.
(a+bi의 절대값은 a^2+b^2, complex는 비교 연산자를 제공하지 않음)
이를 방지하기 위해서 C++11 이상에서 auto를 이용해서 변수의 타입을 추론하게 한다.
template <typename T>
T inline min_magnitue(const T& x, const T& y){
using std::abs;
auto ax=abs(x), ay=abs(y);
return ax<ay?x:y;
}
혹은 더욱 명시적으로 C++의 타입 특성(Type Trait)을 통해 타입의 속성을 제공할 수 있다.
타입 특성은 본질적으로 타입 인수를 갖는 메타 함수이다.
template <typename T>
struct Magnituede{};
template <>
struct Magnitude<int>{
using type=int;
};
template <>
struct Magnitude <float>{
using type=float;
};
template <>
struct Magniutde<std::complex<float>>{
using type=float;
};
template <>
struct Magnitude<std::complex<double>>{
using type=double;
};
첫 번째 코드에서 using type=T;는 생략 가능
template <typename T>
struct Magnitude{
using type=T;
};
위와 같이 내장 타입에 대해 정의해 주면, 올바르게 처리할 수 있다.
하지만, 특수화된 특성이 없는 모든 타입에 잘못 적용된다는 단점을 가지고 있다.
따라서 아래와 같이 complex<float>, complex<double> 등을 각각 정의하는 대신,
모든 complex 타입에 부분 특수화를 사용할 수 있다.
template <typename T>
struct Magnitude<std::complex<T>>{
using type=T;
};
특성을 함수에서 사용
template <typename T>
T inline_min_magnitude(const T& x, const T& y){
using std::abs;
typename Magnitude<T>::type ax=abs(x), ay=abs(y);
return ax<ay? x: y;
}
벡터 및 행렬에서의 놈(norm)의 리턴 타입 결정
template <typename T>
struct Magnitude<vector<T>>{
using type=typename Magnitude<T>::type;
};
조건부 예외 처리함수의 예외 발생 여부는 타입 인수에 따라서 달라진다.
clone함수는 인수 타입에 예외 없는 복사 생성자가 있는 경우, 예외를 던지지 않는다.
표준 라이브러리 <type_traits>에서는 이를 위한 타입 특성을 제공한다.
std::is_nothrow_copy_constructible
위 타입 특성을 이용해서 clone 함수를 예외에서 자유롭다는 것을 표현할 수 있다.
(조건부 noexcept 사용)
#include <type_traits>
inline T clone(const T& x) noexcept(std::is_nothrow_copy_constructible<T>::value){
return T{x};
}
벡터 클래스의 첨자 연산자가 예외를 던지지 않을 때, 예외에서 자유로운 두 벡터의 일반화된 덧셈
template <typename T>
class my_vector{
const T& operator[](int i) const noexcept;
};
template <typename Vector>
inline Vector operator+(const Vector& x, const Vector& y) noexcept(noexcept(x[0])){ ... }
const를 깔끔하게 처리하는 뷰(View)View는 다른 개체에 서로 다른 관점을 제공하는 작은 개체이다.
행렬의 전치로된 행렬을 제공하기 위해 새로운 행렬을 만들면,
메모리 할당 및 할당 해제가 필요하며 전치된 값을 갖는 행렬의 모든 데이터를 복사해야 한다.
View를 사용하면 위와 같은 비용을 사용하지 않고 처리할 수 있다.
template <typename Matrix>
class transposed_view{
public:
using value_type=typename Matrix::value_type;
using size_type=typename Matrix::size_type;
explicit transposed_view(Matrix& A): ref(A) {}
value_type& operator()(size_type r, size_type c){
return ref(c, r);
}
const value_type& operator()(size_type r, size_type c) const {
return ref(c, r);
}
private:
Matrix& ref;
}
Matrix 클래스는 operator()를 통해서 행과 열 인덱스를 나타내는 두 개의 인수를 받으며 해당 항목을 가리키는
레퍼런스를 반환한다고 가정한다. 또한 value_type와 size_type 타입특성이 정의되어 있다고 가정한다.
transposed_view 클래스의 개체는 일반 행렬처럼 취급할 수 있다.
(행렬을 기대 하는 모든 함수 템플릿에 전달할 수 있다.)
교환된 인덱스로 참조된 개체의 operator()를 호출해 즉석해서 전치 동작을 수행한다.
tml::dense2D<float> A={{2, 3, 4}, {5, 6, 7}, {8, 9, 10}};
transposed_view<mtl::dense2D<float>> At(A);
위와 같이 정의했을 경우,
At(i, j)를 호출하면, A(j, i)를 반환한다.
At(2, 0)=4.5; 를 수행하면,
A(0, 2)에 4.5로 설정된다.
template <typename Matix>
inline transposed_view<Matrix> trans(Matrix& A){
return transposed_view<Matrix>(A);
}
위와 같이 전치된 뷰(transposed_view<Matrix>)를 반환하는 함수를 추가해서 간결하게 사용 가능하다.
const 처리하기위 코드는 상수 행렬의 전치된 뷰를 만들 때 오류가 발생할 수 있다.
const mtl::dense2D<float> B(A);
위와 같이 행렬을 생성된 행렬 B의 전치된 행렬 뷰를 만들 수는 있지만,
해당 전치된 행렬 뷰의 요소에는 접근할 수 없다.
cout<<"trans(B)(2, 0) = "<<trans(B)(2, 0)<<'\n';
위 동작을 확인하기 위해서 trans(A)와 trans(B)의 typeid를 확인해 볼 수 있다.
#include <typeinfo>
cout<<"trans(A) is "<<typeid(tst::trans(A)).name()<<'\n';
cout<<"trans(B) is "<<typeid(tst::trans(B)).name()<<'\n';
위의 typeid의 결과로 런타임 타입 식별(Run-Time Type Identification, RTTI)를 출력해준다.
이는 맹글링된 이름이기 때문에 식별이 어렵다.
‘네임 디맹글러(Name Demangler)'를 사용하여 해독할 수 있다.
GNU compiler의 c++filt 도구 사용
명령 파이프에 -t 플래그를 추가( const_view_test | c++filt -t )
typeid를 통해서 확인해 보면,
trans(B)는 템플릿 매개변수 (dense2D<…>가 아닌 const dens2D<…>로 transposed_view를 반환한다.
- trans(B)를 호출하게 되면, 함수의 템플릿 매개변수가 const dense2D<float>로 인스턴스화
- trans(B)의 리턴 타입은 transposed_view<const dense2D<float>>이다.
- transposed_view 생성자 매개변수는 const dens2D<float>& 타입이다.
- transposed_view 멤버 ref는 const dense2D<float>& 타입을 갖는다.
즉 ref 멤버 변수는 const 변수이다. 하지만, 위 코드를 실행하면 const 오버로드가 아닌 일반 오버로드가 실행된다.
오버로드는 ref 멤버 변수의 const 여부와 뷰의 상수 여부를 확인하고 오버로드를 선택한다.
개체인 뷰는 transposed_view<const dense2D<float>> 타입으로 const가 아닌 오버로드가 선택되고,
반환하는 오버로드 ref는 const 타입이기 때문에 오류가 발생한다.
(상수 행렬의 항목을 변경 가능한 레퍼런스로 반환하였기 때문에 오류 발생)
const_cast를 사용해서 const 변수의 const를 벗길 수 있다.
하지만, const를 잘못 사용한 서드 파티 소프트웨어를 처리하는 경우가 아니라면, 사용해서는 안된다.
BLAS, LAPACK 및 이와 유사한 구식 인터페이스와 함께 다른 라이브러리에 const를 정확하게 사용하는
고품질 인터페이스를 제공하는 Boost:Bindings[30]을 사용할 수 있다.
상수 행렬에 두 번째 뷰 클래스를 구현하고, 상수 인수를 받아 이 뷰를 반환하는 trans 함수를 오버로드해 상수 행렬 처리
template <typename T>
class const_transposed_view{
public:
using value_type=typename Matrix::value_type;
using size_type=typename Matrix::size_type;
explicit const_transposed_view(const Matrix& A): ref(A) {}
const value_type& operator()(size_type r, size_type c) const {
return ref(c, r);
}
private:
const Matrix& ref;
};
template <typename matrix>
inline const_transposed_view<Matrix> trans(const Matrix& A){
return const_transposed_view<Matrix>(A);
}
이와 같이 const 를 위한 transposed_view 를 별도로 저장하면, 중복된 코드가 많아진다.
template <typename T>
struct is_const{
static const bool_value=false;
};
template <typename T>
struct is_const<const T>{
static const bool value=true;
};
템플릿 T로 상수 타입이 전달될 경우, 두 정의 모두 일치하지만, 두 번째 정의가 더 구체적(특수화)이므로
컴파일러는 두 번째 정의를 선택한다. 상수가 아닌 타입은 첫 번째 정의만 일치한다.
컴파일 타임 분기(conditional)컴파일 타임 If(Compile-Time If)
template <bool Condition, typename ThenType, typename ElseType>
struct conditional{
using type=ThenType;
};
template<typename ThenType, typename ElseType>
struct conditional<false, ThenType, ElseType>{
using type=ElseType;
};
위 템플릿 메타 함수(conditional)는 논리적 표현식과 두 가지 타입으로 인스턴스화되면,
첫 번째 인수를 계산한 결과가 true라면 맨 위 템플릿만 일치하므로 ThenType이 적용되고,
첫 번째 인수를 계산한 결과가 false라면 특수화가 더 구체적이기 때문에 ElseType이 적용된다.
위와 동일한 conditional 템플릿 메타 함수는 C++11의 일부로 <type_trait>헤더에 정의되어 있다.
using tmp_type=typename conditional<(max_iter>100), double, float>::type;
cout<<"typeid = "<<typeid(tmp_type).name()<<'\n';
단 메타 프로그래밍으로 구현되었기 때문에 max_iter는 컴파일 타임에 알 수 있는 값이어야 한다.
최종 뷰위의 코드에서 상수 행렬의 항목을 변경 가능한 레퍼런스로 반환하였기 때문에 오류가 발생하였다.
template <typename Matrix>
calss transposed_view{
public:
using value_type=Collection<Matrix>::value_type;
using size_type=Collection<Matrix>::size_type;
private:
using vref_type=conditional_t<is_const<Matrix>::type, const value_type&, value_type&>;
public:
transposed_view(Matrix& A): ref(A) {}
vref_type operator()(size_type r, size_type c){
return ref(c, r);
}
const value_type& operator()(size_type r, size_type c) const {
return ref(c, r);
}
private:
Matrix& ref;
};
표준 타입 특성C++ 표준의 타입 특성은 Boost 라이브러 콜렉션에서 비록되었다.
Boost 타입 특성에서 C++ 표준의 타입 특성으로 옮기는 경우, 일부 이름이 다를 수 있음을 유의해야 한다.
도메인 타입 특성표준 라이브러리의 타입 특성 외의 도메인별 타입 특성을 구현
is_matrix
template <typename T>
struct is_matrix{
static const bool value=false;
};
표준 라이브러리는 위의 정적 상수만을 포함하는 false_type를 제공한다.
이 메타 함수를 파생하고, false_type 값을 상속받아 일부 코드를 입력하지 않아도 된다.
template <typename T>
struct is_matrix: std::false_type {};
template <typename Value, typename Para>
struct is_matrix<mtl::dense2D<Value, Para>>: std::true_type {};
template <typename Matrix>
struct is_matrix<trasposed_view<Matrix>> :is_matrix<Matrix> {};
template <typename Matrix>
struct is_matrix<matrix::transposed_view<Matrix>>: std::true_type {};
위에서 transposed_view가 올바르게 사용되는지 확인
static_assert는 런타임에 오버헤드를 발생시키지 않도록, 타입 수준에서 오류 메시지를 출력하게 하거나,
컴파일 타임 상수와 관련해서만 사용해야 한다.
template <typename Matrix>
class transposed_view{
static_assert(is_matrix<Matrix>::value, "Argument of this view must be a matrix!");
}
C++14까지는 오류 메시지가 리터럴이어야만 한다.
위처럼 정적 단정문(static_assert)를 사용하면, 이전에 trans(A), trans(B)를 위해
오버로드를 두 번 작업할 필요가 없다.
template <typename T>
struct is_matrix<const T>: is_matrix<T> {};
is_matrix<T>가 true인 경우, const T 또한 행렬이라고 출력함
enable_ifenable_if는 SFINAE(Substitution Failure Is Not An Error, 추론 실패는 오류가 아님)법칙을 기반으로 한다.
인수 타입으로 추론할 수 없는 헤더를 갖는 함수 템플릿은 무시하며 오류를 발생시키지 않는다.
위와 같은 추론 오류는 함수의 리턴 타입이 템플릿 인수의 메타 함수일 때, 발생할 수 있다.
template <typename T>
typename Magnitude<T>::type inline min_abs(const T& x, const T& y){
using std::abs;
auto ax=abs(x), ay=abs(y);
return ax<ay? ax: ay;
}
위 코드의 리턴 타입은 Magnitude<T> 이다.
Magnitude<T>에 멤버 type이 없다면, 추론은 실패하며 함수 템플릿은 무시하게 된다.
위와 같은 방식은 여러 오버로드가 정확히 하나의 함수로 성공적으로 추론할 수 있을 때,
함수 호출을 컴파일 할 수 있게 한다.
또한 여러 함수 템플릿을 추론할 수 있고, 그 중 하나가 다른 함수 템플릿보다 구체적일 때,
함수 호출을 컴파일 할 수 있다.
주어진 조건이 성립할 때만 실행 가능한 함수 오버로드를 정의할 수 있도록 하는 메타 함수 enable_if
template <bool Cond, typename T=void>
struct enable_if{
typedef T type;
};
template <typename T>
struct enable_if<flase, T> {};
template <bool Cond, typename T=void>
using enable_if_t=typename enable_if<Cond, T>::type;
enable_if 의 조건이 충족할 때만 타입을 정의한다.
제네릭 방식으로 L1 norm 구현
template <typename T>
enable_if_t<is_matrix<T>::value, Magnitude_t<T>> inline one_norm(const T& A){
using std::abs;
Magnitude_t<T> max{0};
for(unsigned c=0; c<num_cols(A); c++){
Magnitude_t<T> sum{0};
for(unsigned r=0; r<num_cols(A); r++) sum+=abs(A[r][c]);
max=max<sum? sum: max;
}
return max;
}
template <typename T>
enable_if_t<is_vector<T>::value, Magnitude_t<T>> inline one_norm(const T& v){
using std::abs;
Magnitude_t<T> sum{0};
for(unsigned r=0; r<size(v); r++) sum+=abs(v[r]);
return sum;
}
첫 번째 one_norm 함수의 템플릿(T)에 is_matrix 함수가 입력될 경우,
1. is_matrix<T>가 true_type으로 계승된다.
2. enable_if_t<>는 Magnitude_t<T>가 된다.
3. 함수 오버로드의 리턴 타입이 Magnitude_t<T>이다.
인수가 행렬 타입이 아닌 경우,
1. is_matrix<T>가 false_type으로 계산된다.
2. enable_if<>::type이 정의되지 않기 때문에 enable_if_t<>를 추론할 수 없다.
3. 함수 오버로드에는 리턴 타입이 없음 -> 무시됨
matrix A={{2, 3, 4}, {5, 6, 7}, {8, 9, 10}};
dense_vector<float> v={3. 4. 5};
cout<<"one_norm(A) is "<<one_norm(A)<<"\n";
cout<<"one_norm(B) is "<<one_norm(B)<<"\n";
행렬 또는 벡터가 아닌 타입의 경우, 사용 가능한 one_norm 오버로드가 없으므로 사용할 수 없다.
행렬과 벡터, 두 타입 모두로 간주되는 모호함을 초래하는 경우, 에러 출력
enable_if 메커니즘은 매우 강력하게 사용할 수 있지만, 디버깅을 복잡하게 만든다.
최신 컴파일러(clang++ > 3.3, gcc > 4.9)는 프로그래머에게 enable_if를 통해 비활성화된
적절한 오버로드가 있다고 알려줌
활성화 메커니즘은 가장 구체적인 조건을 선택할 수 없다.
(is_sparse_matrix와 같은 구현을 특수화할 수 없음)
template <typename T>
enable_if<is_matrix<T>::value && !is_sparse_matrix<T>::value, Magnitude_t<T>> inline one_norm(const T& A);
template <typename T>
enable_if<is_sparse_matrix<T>::value, Magnitude_t<T>>
inline one_norm(const T& A);
SFINAE 패러다임은 함수 자체의 템플릿 인수에만 적용할 수 있다.
멤버함수는 클래스의 템플릿 인수에 enable_if를 적용할 수 없다.
생성자처럼 리턴 타입이 없는 함수에서는 SFINAE 패러다임이 동작하지 않음
가변 템플릿 수정이전에 임의의 인수 개수를 가지는 혼합 타입의 합을 구하는 가변 템플릿 함수 sum에서
적합한 리턴 타입을 정의할 수 없어 실패하였다.
template <typename T>
inline T sum(T t){ return t; }
template <typename T, typename... P>
auto sum(T t, P... p)->decltype(t+sum(p...))
{
return t+sum(p...);
}
위의 코드는 인수가 2개 이상일 경우, 컴파일되지 않는다.
n개의 인수에 대한 리턴 타입을 결정하기 위해서는 지난 n-1개의 인수에 대한 리턴 타입이 필요하다.
리턴 타입 추정(auto)는 함수가 완전히 정의된 후에만 사용할 수 있지만,
위 코드에서는 리턴 타입 추적은 아직 정의되지 않았다.
(재귀적으로 함수가 호출되지만, 재귀적으로 인스턴스화하지 않음)
이를 가변 클래스 템플릿으로 해결할 수 있다.
가변 클래스 템플릿
template <typename ...P>
struct sum_type;
template <typename T>
struct sum_type<T>{
using type=T;
};
template <typename T, typename ...P>
struct sum_type<T, P...>{
using type=decltype(T()+typename sum_type<P...>::type());
};
template <typename ...P>
using sum_type_t=typename sum_type<P...>::type;
위를 구현하기 위해서 두가지 알고리즘이 필요하다.
- n개의 매개변수 관점에서 어떻게 n-1개의 매개변수를 갖는 클래스를 정의할 합성 부분(composite part)
- 0개 또는 1개의 인수에 대한 기저 사례(base case)
재귀 함수와 달리 재귀 클래스에서는 재귀적으로 인스턴스화한다.따라서 가변클래스에서는 decltype을 재귀적으로 사용할 수 있다.
리턴 타입 추론 및 가변 계산 분리
template <typename T>
inline T sum(T t){ return t; }
template <typename T, typename ...P>
inline sum_type_t<T, P...> sum(T t, P... p){
return t+sum(p...);
}
위와 같이 구현할 경우, 에러없이 옳바르게 리턴 타입을 추론한다.
sum_type에서 재귀적으로 최종 리턴함수를 출력
auto s=sum(-7, 3.7f, 9u, -2.6);
cout<<"s is "<<s<<" and its type is "<<typeid(s).name()<<'\n';
auto s2=sum(-7, 3.7f, 9u, -42.6);
cout<<"s2 is "<<s2<<" and its type is "<<typeid(s2).name()<<'\n';
공용 타입(common_type)<type_traits>에 sum_type과 유사한 타입 특성인 std::comon_type을 제공한다.
(C++14에서는 common_type_t 별칭을 도입)
C++타입의 암시적 변환 규칙은 표현식의 결과타입이 연산과는 독립적이다.
x+y+z;
x-y-z;
x*y*z;
x*y+z;
위의 연산과 독립적으로 인수 타입에만 의존해서 결과 타입이 결정된다.
내장 타입의 경우 아래 메타 술어는 항상 true_type으로 계산한다.
is_same<decltype(x+y+z), common_type<decltype(x), decltype(y), decltype(z)>::type>
사용자 정의 타입은 모든 연산에서 동일한 타입을 반환한다고 보장할 수 없다.
따라서 연산에 종속하는 타이 특성을 제공하는 것이 타당하다.
template <typename T>
inline T minimum(const T& t){ return t; }
template <typename T, typename ...P>
typename std::common_type<T, P...>::type minimu(const T& t, const P&... p){
typedef typename std::common_type<T, P...>::type res_type;
return std::min(res_type(t), res_type(minimu(p...)));
}
위의 코드에 대하여서 아래의 연산은 double 형을 반환한다.
minimum(-7, 3.7f, 9u, -2.6)
C++14에서는 템플릿 별칭 및 리턴 타입 추론(auto)을 사용해 minimum의 가변 오버로드를 간소화한다.
template <typename T, typename P...>
inline auto minimu(const T& t, const P&... p){
using res_type=std::common_type<t, P...>;
return std::min(res_type(t), res_type(minimum(p...)));
};
가변 템플릿의 결합위에서의 sum 가변 템플릿 함수에 대하여서 인자의 순서를 바꿔줄 경우 컴파일 되지 않는다.
C++의 경우, 표현식의 왼쪽에서 오른쪽 순서로(left-associative)로 결합한다.
따라서 아래 코드로 구현한 코드는 컴파일 되지 않음
template <typename T>
inline T sum(T t){ return t; }
template <typename ...P, typename T>
typename std::common_type<P..., T>::type sum(P... p, T t){
return sum(p...)+t;
}
정수는 결합 순서가 있다.(계산 순서가 중요하지 않다.)
부동소수점 수는 반올림 오류로 인해 결합 순서가 없다.
가변 템플릿의 사용으로 인한 계산 순서의 변화는 수치적 불안정을 야기하지 않는다.
하지만, 마지막에 가변 인수를 받는 것이 좋은 것 같음